// This program converts text files between using Unix, Windows or (old) Mac
// formats for newlines, i.e. CR, LF and CR+LF.

// Program version: 1.0

// Copyright 2010(c) Lasse Liehu < lliehu at kolumbus dot fi >

/*This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.*/

#include <iostream>
#include <string>
#include <list>
#include <cassert>
#include <fstream>
#include <cstring>
using std::cout;
using std::cerr;
using std::endl;
using std::string;
using std::list;
using std::fstream;

enum EOLFormat { EOL_Unix, EOL_Windows, EOL_Mac };

void show_usage_help();
bool read_cfg_from_params(int argc, char *argv[], EOLFormat& format, list<string>& files);
bool format_EOLs(const string& filename, EOLFormat format);

int main(int argc, char* argv[])
{
    if (argc <= 1) { show_usage_help(); return 0; }
    else
    {
        // Reading command-line parameters
        EOLFormat format;
        list<string> files;
        if (!read_cfg_from_params(argc, argv, format, files) )
            return 1;
        
        // Processing the files
        list<string>::const_iterator filesEnd = files.end();
        for (list<string>::const_iterator iter = files.begin();
             iter != filesEnd; ++iter)
        {
            string tmp = *iter;
            format_EOLs(*iter, format);
        }
        
        // Done, exiting
        return 0;
    }
}

void show_usage_help()
{
    cout << "Use: EOL-formatter -u|-w|-m [file] [file] [...]" << endl << endl;
    cout << "Options:" << endl;
    cout << "-u    format newlines to Unix format ()" << endl;
    cout << "-w    format newlines to Windows format ()" << endl;
    cout << "-m    format newlines to Mac (before OS X) format ()" << endl;
    cout << endl;
    cout << "NOTE: One and exactly one of these options must be chosen." << endl;
    cout << endl;
    cout << "Rest of the parameters are treated as file names to format." << endl;
}

// Returns true if succeeded, false otherwise.
// Parameters `format' and `files' will be filled with appropriate info, but
// only if the function succeeds, i.e. returns true. When it returns false,
// those parameters DO NOT contain the appropriate data.
// Parameters `argc' and `argv' should contain the same data as main()'s.
bool read_cfg_from_params(int argc, char *argv[], EOLFormat& format, list<string>& files)
{
    assert(argc>=2);

    // Reading the type of newline to convert to
    if (string("-u").compare(argv[1]) == 0) { format = EOL_Unix; }
    else if (string("-w").compare(argv[1]) == 0) { format = EOL_Windows; }
    else if (string("-m").compare(argv[1]) == 0) { format = EOL_Mac; }
    else
    {
        cerr << "The type of newline to convert to was not specified as "
             << "the first parameter." << endl
             << "Please specify either -u, -w or -m." << endl;
        return false;
    }

    // Reading the list of files to format
    for (int i = 2; i < argc; ++i)
    {
        files.push_back(string(argv[i]));
    }
    return true;
}

bool format_EOLs(const string& filename, EOLFormat format)
{
    try
    {
        // Open the file for reading
        fstream file;
        file.open(filename.c_str(), fstream::binary | fstream::in);
        if (!file)
        {
            cerr << "Couldn't open file: " << filename.c_str() << endl;
            return false;
        }
        file.exceptions(std::ios::badbit | std::ios::failbit);
    
        // Read the file
        string fileData;
        std::getline(file, fileData, static_cast<char>(EOF) ); // read it till OEF
        file.close();
        
        // Find out the newline format the file uses
        int firstNewlinePos;
        string oldEOL;
        if ( (firstNewlinePos = fileData.find('\r')) != string::npos) // if \r was found
        {
            if (fileData.find('\n', firstNewlinePos+1) == firstNewlinePos+1)
            {
                // Windows type
                oldEOL = "\r\n";
            }
            else
            {
                // old Mac type
                oldEOL = "\r";
            }
        }
        else if (firstNewlinePos = fileData.find('\n') != string::npos) // if \n was found
        {
            // Unix type
            oldEOL = "\n";
        }
        else
        {
            // no recognized newlines in the file, nothing to do
            return true;
        }
        
        // Set the string representation of the target newline format
        string newEOL;
        if (format == EOL_Windows) newEOL = "\r\n";
        else if (format == EOL_Unix) newEOL = "\n";
        else if (format == EOL_Mac) newEOL = "\r";
        else
        {
            cerr << "Internal error!" << endl;
            return false;
        }
        
        // Replace the old newlines with the new ones
        int findIndex = 0;
        while ( (findIndex = fileData.find(oldEOL, findIndex)) != string::npos )
        {
            fileData.erase(findIndex, oldEOL.length());
            fileData.insert(findIndex, newEOL);
            findIndex += newEOL.length();
        }
        
        // Write the file with the new style newlines
        file.open(filename.c_str(), fstream::binary | fstream::out | fstream::trunc);
        file.write(fileData.c_str(), fileData.length() );
        file.close();
        
        // Done
        return true;
    }
    // Handle file I/O errors
    catch (fstream::failure& e)
    {
        // Oh how everyone loves these kind of extremely vague error msgs
        cerr << "Processing of file " << filename << " failed" << endl;
        return false;
    }
}
